Skip to main content

07 - 偏好对齐

SFT 之后的模型会对话了,但「会对话」和「答得好」是两回事。这一篇讲怎么让模型学会在多个都通顺的回答里挑更好的那个,产出 dpo.py

前置:06 篇跑完的 SFT checkpoint。

零、开始之前:SFT 之后还缺什么

0.1 SFT 的天花板在哪

SFT 干的事是模仿:给一堆「问题 → 标准回答」,让模型学着照做。

这套办法有个天生的上限。模仿只能让模型逼近训练数据的水平,没法超过它。 而且模仿是无差别的,训练数据里写得好的和写得一般的,模型一视同仁地学。

更关键的是,SFT 只告诉模型「什么是对的」,从来没告诉它「什么是不好的」。模型不知道哪些回答该避免。

0.2 一个具体的例子

同一个问题,两个都通顺的回答:

问:帮我写一封请假邮件

回答 A:
好的,这是一封请假邮件:
尊敬的领导,我因身体不适需请假一天,望批准。此致敬礼。

回答 B:
好的,这是一封请假邮件:
主题:请假申请(3 月 5 日)
尊敬的张经理:
我因感冒发热需于 3 月 5 日请假一天,期间工作已交接给李明,
紧急事项可电话联系我。给您带来不便,敬请谅解。
王强
2026 年 3 月 4 日

两个都没错,语法都通,都是「请假邮件」。但 B 明显更好。

SFT 没法表达这种差别——它的数据里只有一个标准答案,没有「这两个之中 B 更好」这种信息。

偏好对齐要做的就是把这种比较信息喂给模型。

SFT 的天花板:模仿只能逼近训练数据的水平,没法超过它帮我写一封请假邮件同一个问题�回答 A尊敬的领导,我因身体不适需请假一天,望批准。回答 B主题:请假申请(3 月 5 日)· 事由 · 交接给李明· 紧急可电话联系 · 落款与日期两个都没错,语法都通,都是「请假邮件」—— 但 B 明显更好而 SFT 的数据长这样{"user": 问题, "assistant": 一个标准答案}没有第二个答案,也就没有「B 比 A 好」这条信息模仿是无差别的:训练数据里写得好的和写得一般的,模型一视同仁地学。SFT 只告诉了模型「什么是对的」,从来没告诉它「什么是不好的」。偏好对齐要做的,就是把「这两个之中哪个更好」这种比较信息喂给模型 —— 这是 SFT 的数据格式根本装不下的东西。
关键不在于 A 写得差,而在于 A 完全合格。SFT 的损失函数没有任何一项能区分「合格」和「更好」,所以哪怕训练数据里全是 B 这样的回答,模型学到的也只是「像 B 一样写」,而不是「B 比 A 好」这条可以外推的判断。

0.3 偏好数据长什么样

不再是「问题 + 答案」,而是「问题 + 更好的答案 + 更差的答案」:

{
"prompt": [{"role": "user", "content": "帮我写一封请假邮件"}],
"chosen": "主题:请假申请……(详细版)",
"rejected": "尊敬的领导,我因身体不适需请假一天……(简略版)"
}

行业黑话叫 chosen 和 rejected,或者 ywy_w(win)和 yly_l(lose)。

标注这种数据比标注 SFT 数据容易得多:让人从头写一个好回答很难,但让人在两个回答里选一个更好的,很快。这是偏好学习能规模化的原因。

样本结构变了一处:多了一个「更差的答案」SFT 样本prompt 用户的问题answer 一个标准答案—— 只有「对」这一个维度偏好样本prompt  用户的问题chosen  更好的答案(黑话 y_w,win)rejected 更差的答案(黑话 y_l,lose)标注成本才是这件事能规模化的原因从头写一个好回答慢,而且写得好不好取决于标注员水平在两个回答里选一个快得多,而且判断力比创作力更容易找到这条成本差异是偏好学习能做大的根本原因:判断比创作便宜。同样的人力预算,能标出的偏好对比 SFT 样本多一个量级。
数据结构上只多了一个字段,工程上却打开了一个新维度:模型第一次拿到了「相对」的信号。后面 DPO 的整个推导都建立在这一点上 —— 它的损失函数里,chosen 和 rejected 永远是成对出现、相减的。

0.4 三条路线

从偏好数据到对齐的模型,主流有三条路:

路线一句话出现时间
PPO先训个奖励模型打分,再用强化学习去最大化分数2022,ChatGPT 用的
DPO数学上把 RL 消掉,变成一个监督学习问题2023
GRPO回到 RL,但去掉最重的那个部件2024,DeepSeek 推广

这一篇以 DPO 为主(简单、稳、单卡跑得动),但会先讲 PPO,因为不理解 PPO 就理解不了 DPO 在消掉什么。最后讲 GRPO 为什么又转回 RL。

三条路线,以及它们各自在减掉什么时间2022 · PPO(经典 RLHF)先训个奖励模型打分,再用强化学习最大化分数同时驻留 4 个模型 · 18.08 GBChatGPT 用的就是这条2023 · DPO —— 本篇主线数学上把 RL 整个消掉,变成一个监督学习问题只剩 2 个模型 · 9.04 GB减掉的是:奖励模型 + critic + 采样2024 · GRPO回到 RL,但去掉最重的那个部件 —— critic3 个模型 · 10.04 GBDeepSeek 推广,擅长数学和代码本篇以 DPO 为主(简单、稳、单卡跑得动),但会先讲 PPO —— 不理解 PPO 就理解不了 DPO 在消掉什么,最后讲 GRPO 为什么又转回 RL。注意这不是一条「越新越好」的直线:DPO 减掉的东西,GRPO 又捡回来了一部分,因为那些东西在推理类任务上是有用的。
三条路线在时间上是顺序的,在能力上不是。DPO 用一个巧妙的数学变换换来了极大的工程简化,代价是失去在线采样和探索;GRPO 承认这个代价在数学、代码这类任务上付不起,于是回到 RL,只把 PPO 里最贵的那件东西扔掉。

一、经典 RLHF:PPO 那条路

1.1 三个阶段

06 篇做完的是阶段一。

1.2 奖励模型

阶段二训一个奖励模型(reward model,RM)。它的输入是「问题 + 回答」,输出是一个标量分数。

训练方式是让它在偏好对上排序正确,loss 是:

LRM=logσ(r(x,yw)r(x,yl))\mathcal{L}_{\text{RM}} = -\log \sigma\big(r(x, y_w) - r(x, y_l)\big)

含义很直白:让 chosen 的分数高于 rejected,差距越大 loss 越小。σ\sigma 是 sigmoid。

记住这个式子的形状,DPO 的 loss 跟它长得极像,这不是巧合。

1.3 PPO 阶段要同时装四个模型

阶段三用强化学习。模型生成回答,奖励模型打分,用分数当奖励信号去更新模型。

麻烦在于显存里要同时驻留四个模型:

模型作用要训吗
policy正在训练的模型
refSFT 后的模型,冻结,用来算 KL 约束
reward打分
critic估计状态价值,给 PPO 算优势函数

三条路线各要驻留几个模型,是它们工程复杂度的核心差异:

PPO:四个模型同时在显存里policy可训 · 8.04 GBref冻结 · 1.00 GBreward冻结 · 1.00 GBcritic可训 · 8.04 GB36 字节/参数 18.08 GBGRPO:去掉 critic,用组内平均分当基线policy可训 · 8.04 GBref冻结 · 1.00 GBreward冻结 · 1.00 GBcritic 省掉20 字节/参数 10.04 GBDPO:连奖励模型都不要policy可训 · 8.04 GBref冻结 · 1.00 GBreward 和 critic 都省掉,偏好数据直接当监督信号18 字节/参数 9.04 GBDPO 还能再省 1 GB偏好数据固定、ref 冻结,两个 ref_logp 可离线预算好存盘,训练时不必驻留 ref 模型,降到 8.04 GB。但显存不是全部GRPO 每条样本要在线生成 G 个回答(通常 8–16 个),生成本身很慢,实际耗时远高于 DPO,尽管只多 1 GB。
PPO 的 critic 与 policy 同样大且同样要训,占了显存的一半,这是它工程复杂度高的主要来源。GRPO 用一组采样的平均分替代 critic 的价值估计,DPO 则通过 2.2 节的推导把奖励模型也消掉,只剩策略与参考两个模型。

code/align_math.py 算出来的账:

路线                    模型数      字节/参数        显存
PPO(经典 RLHF) 4 36 18.08 GB
GRPO 3 20 10.04 GB
DPO 2 18 9.04 GB

PPO 要 18.08 GB,是 DPO 的两倍。而且 PPO 训练过程中要不断采样生成,超参多、调起来不稳,工程复杂度远高于另外两条路。

1.4 KL 惩罚在防什么

PPO 的目标函数里有一项 KL 惩罚,约束 policy 别离 ref 太远:

maxπ E[r(x,y)]βKL(ππref)\max_\pi \ \mathbb{E}\big[r(x,y)\big] - \beta\, \mathrm{KL}\big(\pi \,\|\, \pi_{\text{ref}}\big)

为什么需要它。因为奖励模型是个近似,它有漏洞。如果放任模型只管把分数刷高,它会找到奖励模型的破绽,生成一些分数极高但人看了莫名其妙的东西。这叫奖励攻陷(reward hacking)。

KL 惩罚就是拴住模型的绳子:可以变好,但别变得面目全非。

这一项在 DPO 里也在,只是形式变了,2.2 节会看到。

奖励模型是个近似,它一定有漏洞 —— KL 惩罚就是拴住模型的绳子policy正在被优化的模型❌ 只管把奖励刷高max E[ r(x, y) ]目标里没有任何约束项奖励攻陷(reward hacking)找到奖励模型的破绽,生成分数极高但人看了莫名其妙的东西✅ 加上 KL 惩罚max E[ r ] − β · KL( π ‖ π_ref )离参考模型越远,罚得越狠可以变好,但别变得面目全非β 越大绳子越短;这一项在 DPO 里也在,只是换了形式DPO 的损失里没有显式的 KL 项,但那根绳子藏在 π / π_ref 这个比值里:分母那个参考模型一直在盯着,模型不能无节制地改变概率分布。
奖励攻陷不是罕见的失败,是无约束优化的默认结局:只要奖励是学出来的近似,优化器就一定会去找它和真实偏好之间的缝隙。所以这一项不是调优手段,是必需品 —— 记住这一点,2.3 节看到 DPO 里那个比值时才知道它是从哪来的。

二、DPO:把 RL 消掉

2.1 核心洞察

DPO 那篇论文(Rafailov et al., 2023)的关键发现是:1.4 节那个「带 KL 约束的奖励最大化」问题,有闭式解。

解出来长这样:

π(yx)=1Z(x)πref(yx)exp ⁣(1βr(x,y))\pi^*(y \mid x) = \frac{1}{Z(x)}\, \pi_{\text{ref}}(y \mid x)\, \exp\!\left(\frac{1}{\beta} r(x,y)\right)

意思是最优策略等于参考模型乘一个跟奖励有关的指数项。Z(x)Z(x) 是归一化因子。

这个式子本身没法直接用,因为 Z(x)Z(x) 要对所有可能的回答求和,算不出来。

但可以把它反过来解出 rr

r(x,y)=βlogπ(yx)πref(yx)+βlogZ(x)r(x,y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\text{ref}}(y \mid x)} + \beta \log Z(x)

这一步是整个 DPO 的转折点。它说明:奖励函数可以用策略模型自己表示出来,不需要单独训一个奖励模型。

2.2 Z(x)Z(x) 怎么消掉的

上面那个式子还带着算不出来的 Z(x)Z(x)。但注意 1.2 节奖励模型的 loss 里,rr 只以差值的形式出现:r(x,yw)r(x,yl)r(x,y_w) - r(x,y_l)

两个回答对应同一个问题 xx,所以它们的 βlogZ(x)\beta \log Z(x) 完全相同,一减就没了:

r(x,yw)r(x,yl)=βlogπ(ywx)πref(ywx)βlogπ(ylx)πref(ylx)r(x,y_w) - r(x,y_l) = \beta \log \frac{\pi(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \log \frac{\pi(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}

代回 1.2 节那个 loss,就得到 DPO 的目标:

LDPO=logσ(βlogπ(ywx)πref(ywx)βlogπ(ylx)πref(ylx))\mathcal{L}_{\text{DPO}} = -\log \sigma\left(\beta \log \frac{\pi(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \log \frac{\pi(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}\right)

没有奖励模型,没有 critic,没有采样,没有 RL。 就是一个可以直接反向传播的监督损失。

这就是 DPO 这个名字的由来:Direct Preference Optimization,直接用偏好优化,中间不绕奖励模型。

DPO 的整条推导链:RL 是怎么被消掉的① 出发点1.4 节那个带 KL 约束的奖励最大化问题max E[r] − β·KL要 RL 才解得动② 它有闭式解π* = (1/Z) · π_ref   · exp(r/β)最优策略 = 参考模型乘一个跟奖励有关的指数项③ 反过来解出 rr = β log(π*/π_ref)  + β log Z(x)奖励可以用策略自己表示,但 Z(x) 要对所有回答求和,算不出来④ Z(x) 自己消掉奖励模型的 loss 里,r只以差值出现r(x,y_w) − r(x,y_l)同一个 x,两项的β log Z(x) 一减就没了⑤ DPO 损失−log σ( β·[ 隐式奖励差 ] )chosen 的概率要相对涨rejected 的要相对跌可以直接反向传播走完这五步之后:没有奖励模型,没有 critic,没有采样,没有 RL �—— 剩下的就是一个普通的监督损失。这就是 Direct Preference Optimization 这个名字的由来。KL 那根绳子并没有消失,它藏进了 π / π_ref 这个比值里:每一项算的都是「当前模型给这个回答的概率,相对参考模型涨了还是跌了」。第 ④ 步是全链条的枢纽。它成立的前提是 chosen 和 rejected 来自同一个 prompt —— 这也是偏好数据必须成对的原因。
这五格里最该慢下来看的是 ③ 到 ④:③ 得到的式子带着一个算不出来的 Z(x),看起来是死路;④ 发现它根本不需要被算出来,因为它在差值里自己抵消了。整个 DPO 的价值就压在这一步上,其余四步都是常规推导。

2.3 每一项在做什么

把式子拆开看:

logπ(ywx)πref(ywx)隐式奖励 r^wlogπ(ylx)πref(ylx)隐式奖励 r^l\underbrace{\log \frac{\pi(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)}}_{\text{隐式奖励}\ \hat{r}_w} \quad - \quad \underbrace{\log \frac{\pi(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}}_{\text{隐式奖励}\ \hat{r}_l}

每一项是「当前模型给这个回答的概率,相对参考模型涨了还是跌了」。涨了就是正奖励,跌了就是负奖励。

优化目标是让 r^wr^l\hat{r}_w - \hat{r}_l 尽量大,也就是:对 chosen 的概率要相对涨,对 rejected 的概率要相对跌。

注意这里是相对参考模型。KL 约束就藏在这个比值里:模型不能无节制地改变概率分布,因为分母那个 πref\pi_{\text{ref}} 一直在盯着。1.4 节那根绳子还在,只是换了个形式。

对数概率参考模型 π_ref 的位置(基准线)chosenr̂_wrejectedr̂_l(负值)margin = r̂_w − r̂_l,DPO 要最大化的就是它隐式奖励 = log(π / π_ref),即相对基准线偏移了多少监控要点:reward_chosen 经常是负的且一路下降,看着像训崩了,但只要 rejected 跌得更快,margin 就在扩大。盯 margin 和 acc(chosen 奖励高于 rejected 的比例,应从 0.5 涨到 0.7 以上),不要盯 reward_chosen 的绝对值。
竖线是参考模型的基准位置,两个点是策略模型给 chosen 和 rejected 的对数概率。DPO 不关心两点的绝对位置,只关心它们之间的距离 margin。这解释了为什么 reward_chosen 为负也完全正常。

2.4 beta 的作用

β\beta 控制约束的松紧:

beta效果
小(0.01 到 0.05)约束松,模型敢大幅偏离 SFT,可能学得快但也容易跑偏
0.1通行默认值
大(0.5 以上)约束紧,几乎不动,训了跟没训一样

从 0.1 开始试,基本不会错。

三、代码

3.1 算序列的对数概率

DPO 的 loss 需要「模型给某个回答的对数概率」,也就是回答里每个 token 的 log-prob 求和:

def sequence_logp(model, input_ids, labels):
logits, _ = model(input_ids)
logits = logits[:, :-1, :] # 错位一格,同 SFT
labels = labels[:, 1:]

mask = labels != IGNORE
safe = labels.masked_fill(~mask, 0) # gather 不接受负索引,先填 0

logprobs = F.log_softmax(logits.float(), dim=-1)
token_logp = torch.gather(logprobs, dim=2, index=safe.unsqueeze(2)).squeeze(2)
return (token_logp * mask).sum(dim=-1) # 被 mask 的位置贡献 0

三个细节:

labels 的构造复用 06 篇的 ChatTemplate,prompt 部分是 IGNORE,只在回答上算。这跟 SFT 是一致的,DPO 关心的也只是回答部分的概率。

masked_fill(~mask, 0) 那行是必须的。 IGNORE 是 -100,而 torch.gather 不接受负索引,会直接报错。先替换成 0,这些位置随后乘 mask 时会归零,不影响结果。

log_softmax 前转 fp32。 概率连乘在对数空间是求和,序列几百个 token 加下来,bf16 的精度不够,会累积明显误差。

3.2 loss

def dpo_loss(policy_logp, ref_logp, beta):
b = policy_logp.shape[0] // 2
pi_c, pi_r = policy_logp[:b], policy_logp[b:]
ref_c, ref_r = ref_logp[:b], ref_logp[b:]

reward_c = pi_c - ref_c # 就是 2.3 节的隐式奖励
reward_r = pi_r - ref_r
logits = beta * (reward_c - reward_r)

loss = -F.logsigmoid(logits).mean()
...

对着 2.2 节那个公式看,一一对应:pi_c - ref_c 就是 logπ(yw)πref(yw)\log \frac{\pi(y_w)}{\pi_{\text{ref}}(y_w)}(对数相减等于比值取对数)。

F.logsigmoid 而不是 torch.log(torch.sigmoid(x)),前者数值稳定,后者在 x 很负时会算出 -inf

3.3 一次前向拿到两边

chosen 和 rejected 拼进同一个 batch,前一半 chosen、后一半 rejected,一次前向搞定:

def collate(batch, pad_id):
seqs = [s for pair in batch for s in pair]
chosen, rejected = seqs[0::2], seqs[1::2]
ordered = chosen + rejected # 前一半 chosen,后一半 rejected
...

所以 dpo_loss 里用 policy_logp[:b][b:] 就能切开。

3.4 ref 模型怎么省

align_math.py 最后那段提到一个优化:

DPO 的 ref 前向可以预计算:偏好数据是固定的,
ref 模型也是冻结的,所以 ref 的两个 logp 可以离线算好存起来,
训练时就不用把 ref 模型放显存里了,降到 8.04 GB。

思路是:ref_logp 只依赖数据和冻结的 ref 模型,训练过程中永远不变。那就没必要每步重算,更没必要把 ref 模型一直放在显存里。

做法是训练前跑一遍全量数据,把每条样本的两个 ref_logp 算好存进文件,训练时直接读。省 1 GB 显存,还省掉一半前向计算。

trl 库里这个功能叫 precompute_ref_log_probs。数据量大的时候值得开。

四、超参

超参SFTDPO为什么
学习率2e-55e-7又小两个数量级
epoch2 到 31极容易过拟合
beta0.1见 2.4 节
batch size164一条样本要跑两个序列

学习率是这一篇最要紧的超参。 DPO 对学习率极其敏感,5e-7 这个量级看着离谱,但确实是通行值。大了会直接把 SFT 学到的能力冲垮,表现是模型开始输出乱码或者疯狂重复。

epoch 就跑 1 轮。 DPO 过拟合非常快,第二轮往往就开始退化。

五、GRPO:为什么又转回 RL

5.1 DPO 的局限

DPO 简单好用,但有几个绕不过去的限制。

它是离线的。 偏好数据是事先标好的,里面的回答不是当前模型生成的。训着训着模型就偏离了数据的分布,这些数据对它的指导意义在下降。

它只能处理成对比较。 有些任务的好坏是可以直接判定的,比如数学题答案对不对、代码能不能跑通。这种情况下有明确的奖励信号,硬要构造成偏好对反而丢失信息。

它没法从探索中学习。 模型只能在给定的两个回答里学,没机会自己生成一堆再从中发现更好的。

对于推理类任务(数学、代码),这几点都是硬伤。所以又转回了 RL。

5.2 GRPO 的想法

GRPO(Group Relative Policy Optimization)要解决的是 PPO 最重的那个部件:critic

回顾 1.3 节,PPO 的 critic 跟 policy 一样大、一样要训,占了显存的一半。它的作用是估计「基线」——判断当前这个回答比平均水平好多少。

GRPO 的做法很直接:既然要基线,那就对同一个问题生成一组(G 个)回答,用这组的平均分当基线。 不需要 critic 了。

优势函数变成组内的相对好坏:

Ai=rimean(r1,,rG)std(r1,,rG)A_i = \frac{r_i - \text{mean}(r_1, \dots, r_G)}{\text{std}(r_1, \dots, r_G)}

比平均分高的回答,梯度推着模型多产出这种;低的,推着少产出。

5.3 三条路线对比

路线                    模型数      字节/参数        显存
PPO(经典 RLHF) 4 36 18.08 GB
GRPO 3 20 10.04 GB
DPO 2 18 9.04 GB

显存上 GRPO 比 PPO 省了 8 GB,就是省掉的 critic。

但要注意显存不是全部。GRPO 每条样本要生成 G 个回答(通常 8 到 16 个),生成本身很慢。所以 GRPO 的实际训练时间远长于 DPO,尽管显存只多 1 GB。

训练一步各自要做什么(块长 = 大致耗时)DPO2 个模型一次前向:policy / ref反传更新9.04 GB · 最快 · 离线,数据是事先标好的GRPO3 个模型对同一问题生成 G 个回答(通常 8 到 16 个)—— 慢就慢在这里打分 / 规则判定组内标准化更新10.04 GB —— 显存只比 DPO 多 1 GB,但实际训练时间远长于 DPO,代价全在生成上PPO4 个模型在线采样生成奖励模型打分critic 估基线更新 policy + 更新 critic18.08 GB · 最慢 —— critic 跟 policy 一样大、一样要训,占了显存的一半,GRPO 砍掉的就是它选型:通用对话偏好 → DPO;数学、代码这类有明确对错的 → GRPO;PPO 通用但工程复杂。这个专题选 DPO:0.5B 做��不了复杂推理,GRPO「生成一组再挑」的优势体现不出来。
「显存不是全部」在这张图上最直观:GRPO 那一行的显存条只比 DPO 长一点,但时间条长了一倍多。选方案时如果只看那张显存表,会得出 GRPO 和 DPO 差不多的错误结论 —— 真正的差别在第一格的宽度里。
DPOGRPOPPO
要奖励模型吗不要要(或用规则判定)
要 critic 吗不要不要
在线采样不用要,每条生成 G 个
显存9.04 GB10.04 GB18.08 GB
训练速度慢(要生成)最慢
适合什么通用对话偏好数学、代码这类有明确对错的通用,但工程复杂

5.4 这个专题选 DPO

理由:0.5B 的模型做不了复杂推理,GRPO 那套「生成一组再挑」的优势体现不出来;而 DPO 单卡就能跑,一两个小时出结果,适合把流程走通。

想试 GRPO 的话,trlverl 都有现成实现,00 篇参考资料里列了。

六、数据从哪来

2026-08-19 用 HuggingFace API 实测:

数据集行数体积许可说明
HuggingFaceH4/ultrafeedback_binarized187,405650.0 MBMIT最常用的通用偏好数据
Anthropic/hh-rlhf169,352MIT经典的有用性和无害性偏好
nvidia/HelpSteer221,36211.9 MBCC-BY-4.0多维度评分,质量高
llamafactory/DPO-En-Zh-20k19,99779.9 MBApache-2.0中英双语,适合我们
argilla/dpo-mix-7k7,50024.2 MBMIT小而精,快速验证用

建议先用 argilla/dpo-mix-7k 把流程跑通(7500 条,很快),再换 llamafactory/DPO-En-Zh-20k 正式训。

七、训练时盯什么

DPO 的 loss 数值本身不太直观,真正要看的是这几个指标,dpo_loss 里都返回了:

指标健康的样子不对劲说明什么
acc从 0.5 涨到 0.7 以上一直在 0.5 说明完全没学到
reward_margin稳定上升不涨说明学习率太小或数据有问题
reward_chosen小幅波动,可正可负暴跌说明模型在崩
reward_rejected应该下降上升说明方向反了

acc 是最直观的:模型给 chosen 的隐式奖励高于 rejected 的比例。训练开始时应该在 0.5 附近(随机),然后往上走。

一个容易误判为故障的现象reward_chosen 经常为负且持续下降。这是正常的。DPO 优化的是两者的差值,只要 reward_rejected 跌得更快,差值就在扩大,目标就在达成。看 marginacc,别盯着 reward_chosen 的绝对值。

八、验收

  • 数据里 chosen 和 rejected 的 prompt 部分确认完全一致
  • sequence_logp 只在回答部分累加,prompt 部分是 IGNORE
  • 初始 acc 在 0.5 附近
  • 训练过程中 acc 涨到 0.65 以上
  • reward_margin 稳定上升
  • 训完模型仍然会正常对话,没有退化成乱码或复读
  • 拿几个问题对比 SFT 模型和 DPO 模型的输出,能看出差别

最后一条是真正的验收。如果 DPO 前后输出看不出任何差别,说明 beta 太大或者学习率太小,等于没训。 反过来如果模型开始胡言乱语,是学习率太大。

九、常见问题

现象大概率是什么原因
torch.gather 报索引越界IGNORE 是 -100,没转成 0,见 3.1 节
训完输出乱码或疯狂重复学习率太大,DPO 要 5e-7 量级
训完跟没训一样beta 太大,或者学习率太小
acc 一直在 0.5chosen 和 rejected 拼接顺序错了,检查 collate
reward_chosen 一直跌正常现象,看 marginacc,见第七节
loss 降但模型变差过拟合,epoch 减到 1
显存不够precompute_ref_log_probs,省掉 ref 模型
logp 数值明显不对log_softmax 前没转 fp32

DPO 的收益是有限的

0.5B 模型做完 DPO,能看到的改善主要是回答更完整、格式更规整。别期待它变得会推理——那是模型规模和预训练数据决定的。这一篇的价值在于把偏好对齐这条链路走通,而不是把模型救成一个好用的助手。